/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.Services.Client;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Xceed.Silverlight.Data
{
  public abstract class WcfDataServicesDataSourceProviderBase<TElement> : WcfDataServicesDataSourceProviderBase
  {
    #region Static Fields

    private static readonly Guid ExecuteCountQueryCallId = DataUtils.CreateCallId();
    private static readonly Guid ExecuteDataQueryCallId = DataUtils.CreateCallId();

    #endregion

    #region Constructors

    protected WcfDataServicesDataSourceProviderBase(
      DataServiceQuery<TElement> dataServiceQuery,
      DataServiceContext dataServiceContext )
      : base( dataServiceContext )
    {
      if( dataServiceQuery == null )
        throw new ArgumentNullException( "dataServiceQuery" );

      this.Initialize( dataServiceQuery );
    }

    internal WcfDataServicesDataSourceProviderBase(
      DataServiceQuery<TElement> dataServiceQuery )
      : base()
    {
      if( dataServiceQuery == null )
        throw new ArgumentNullException( "dataServiceQuery" );

      this.Initialize( dataServiceQuery );
    }

    #endregion

    #region CanCount Property

    public override bool CanCount
    {
      get
      {
        return m_canCount;
      }
    }

    private bool m_canCount; //false

    #endregion

    #region DataServiceQuery Protected Property

    protected DataServiceQuery<TElement> DataServiceQuery
    {
      get;
      private set;
    }

    #endregion

    #region ElementType Protected Property

    protected internal sealed override Type ElementType
    {
      get
      {
        return this.DataServiceQuery.ElementType;
      }
    }

    #endregion

    protected virtual DataServiceQuery<TElement> GetDataServiceQuery( CountQueryParameters queryParameters )
    {
      Debug.Assert( ( queryParameters.FilterGroup == null )
                 && ( queryParameters.Jump == null ) );

      DataServiceQuery<TElement> dataServiceQuery = this.DataServiceQuery;

      if( queryParameters.Filter != null )
      {
        SortRequest sortBy = ( queryParameters.Sort != null )
                               ? queryParameters.Sort
                               : new SortRequest( this.PrimaryKeyInfo );

        dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.OrderBy( dataServiceQuery, sortBy );
        dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.Where( dataServiceQuery, queryParameters.Filter );
      }

      dataServiceQuery = ( DataServiceQuery<TElement> )dataServiceQuery.Take( 0 );

      return dataServiceQuery;
    }

    protected virtual DataServiceQuery<TElement> GetDataServiceQuery( DataQueryParameters queryParameters )
    {
      Debug.Assert( ( queryParameters.ItemJump == null )
                 && ( queryParameters.Jump == null ) );

      DataServiceQuery<TElement> dataServiceQuery = this.DataServiceQuery;

      dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.OrderBy( dataServiceQuery, queryParameters.Sort );

      if( queryParameters.Filter != null )
      {
        if( queryParameters.Sort == null )
        {
          dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.OrderBy( dataServiceQuery, new SortRequest( this.PrimaryKeyInfo ) );
        }

        dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.Where( dataServiceQuery, queryParameters.Filter );
      }

      dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.Skip( dataServiceQuery, queryParameters.Skip );
      dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.Take( dataServiceQuery, queryParameters.Take );

      return dataServiceQuery;
    }

    protected override IAsyncResult BeginExecuteCountQuery(
      CountQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      AsyncResult asyncResult = new AsyncResult(
                                  WcfDataServicesDataSourceProviderBase<TElement>.ExecuteCountQueryCallId,
                                  callback,
                                  state );

      DataServiceQuery<TElement> query = this.GetDataServiceQuery( queryParameters );

      asyncResult.Start(
        ( cb, st ) => query.BeginExecute( cb, query ),
        null );

      return asyncResult;
    }

    protected override CountQueryResult EndExecuteCountQuery( IAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( WcfDataServicesDataSourceProviderBase<TElement>.ExecuteCountQueryCallId );

      IAsyncResult ar = asyncResult.GetLastAsyncResult<IAsyncResult>();
      DataServiceQuery<TElement> query = ( DataServiceQuery<TElement> )ar.AsyncState;
      QueryOperationResponse response = ( QueryOperationResponse )query.EndExecute( ar );

      if( response.Error != null )
        throw response.Error;

      long count = response.TotalCount;

      return new CountQueryResult( count );
    }

    protected override IAsyncResult BeginExecuteDataQuery(
      DataQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      AsyncResult asyncResult = new AsyncResult(
                                  WcfDataServicesDataSourceProviderBase<TElement>.ExecuteDataQueryCallId,
                                  callback,
                                  state );

      DataServiceQuery<TElement> query = this.GetDataServiceQuery( queryParameters );
      PartialDataResult queryResult = new PartialDataResult( query );

      asyncResult.Start(
        ( cb, st ) => query.BeginExecute( cb, queryResult ),
        ( par, iar ) => this.HandleContinuation( iar ) );

      return asyncResult;
    }

    protected override DataQueryResult EndExecuteDataQuery( IAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( WcfDataServicesDataSourceProviderBase<TElement>.ExecuteDataQueryCallId );

      PartialDataResult queryResult = asyncResult.GetCallResult<PartialDataResult>();

      Debug.Assert( queryResult != null );

      if( queryResult.Error != null )
        throw queryResult.Error;

      return new DataQueryResult( queryResult.Data, queryResult.InlineCount );
    }

    private Nullable<long> GetInlineCount( QueryOperationResponse response )
    {
      if( WcfDataServicesDataSourceProvider<TElement>.IsCountingEnabled( response.Query.RequestUri ) )
        return response.TotalCount;

      return null;
    }

    private DependentCallResult HandleContinuation( IAsyncResult asyncResult )
    {
      PartialDataResult queryResult = ( PartialDataResult )asyncResult.AsyncState;
      DataServiceQuery<TElement> query = queryResult.DataServiceQuery;
      QueryOperationResponse<TElement> response = ( QueryOperationResponse<TElement> )query.EndExecute( asyncResult );

      // An error occured, there is nothing left to fetch. 
      if( response.Error != null )
      {
        queryResult.Error = response.Error;

        return new DependentCallResult( queryResult );
      }

      // Retrieve the inline count.
      queryResult.InlineCount = this.GetInlineCount( response );

      // Retrieve the items downloaded.
      queryResult.Data.AddRange( response );

      // Find out if there is a continuation.
      DataServiceQueryContinuation<TElement> continuation = response.GetContinuation();

      // There is nothing left.  We have all the data that was requested.
      if( continuation == null )
        return new DependentCallResult( queryResult );

      // We cannot handle a continuation if we don't have the DataServiceContext.
      if( this.DataServiceContext == null )
      {
        try
        {
          // The exception must be thrown on the call to EndExecuteDataQuery.
          // We throw the exception within a try/catch block to get a call stack.
          throw new DataInternalException();
        }
        catch( Exception e )
        {
          queryResult.Error = e;
        }

        return new DependentCallResult( queryResult );
      }

      return new DependentCallResult(
        queryResult,
        ( cb, st ) => this.BeginExecuteDataContinuation( continuation, cb, st ),
        ( par, iar ) => this.EndExecuteDataContinuation( iar ) );
    }

    private IAsyncResult BeginExecuteDataContinuation(
      DataServiceQueryContinuation<TElement> continuation,
      AsyncCallback callback,
      object state )
    {
      return this.DataServiceContext.BeginExecute( continuation, callback, state );
    }

    private DependentCallResult EndExecuteDataContinuation( IAsyncResult asyncResult )
    {
      PartialDataResult queryResult = ( PartialDataResult )asyncResult.AsyncState;
      QueryOperationResponse<TElement> response = ( QueryOperationResponse<TElement> )this.DataServiceContext.EndExecute<TElement>( asyncResult );

      // An error occured, there is nothing left to fetch. 
      if( response.Error != null )
      {
        queryResult.Error = response.Error;

        return new DependentCallResult( queryResult );
      }

      // Retrieve the items downloaded.
      queryResult.Data.AddRange( response );

      // Find out if there is a continuation.
      DataServiceQueryContinuation<TElement> continuation = response.GetContinuation();

      // There is nothing left.  We have all the data that was requested.
      if( continuation == null )
        return new DependentCallResult( queryResult );

      return new DependentCallResult(
        queryResult,
        ( cb, st ) => this.BeginExecuteDataContinuation( continuation, cb, st ),
        ( par, iar ) => this.EndExecuteDataContinuation( iar ) );
    }

    private void Initialize( DataServiceQuery<TElement> dataServiceQuery )
    {
      this.DataServiceQuery = dataServiceQuery;

      m_canCount = WcfDataServicesDataSourceProvider<TElement>.IsCountingEnabled( dataServiceQuery.RequestUri );
    }

    private static bool IsCountingEnabled( Uri uri )
    {
      return ( uri != null )
          && ( uri.Query.ToLowerInvariant().Contains( "$inlinecount=allpages" ) );
    }

    private static DataServiceQuery<TElement> Take(
      DataServiceQuery<TElement> dataServiceQuery,
      TakeRequest request )
    {
      if( request == null )
        return dataServiceQuery;

      return ( DataServiceQuery<TElement> )dataServiceQuery.Take<TElement>( request.Count );
    }

    private static DataServiceQuery<TElement> Skip(
      DataServiceQuery<TElement> dataServiceQuery,
      SkipRequest request )
    {
      if( ( request == null ) || ( request.Count == 0 ) )
        return dataServiceQuery;

      IQueryable<TElement> query = dataServiceQuery;
      long skip = request.Count;

      while( skip > 0 )
      {
        if( skip > int.MaxValue )
        {
          query = query.Skip<TElement>( int.MaxValue );
          skip -= int.MaxValue;
        }
        else
        {
          query = query.Skip<TElement>( Convert.ToInt32( skip ) );
          skip = 0;
        }
      }

      return ( DataServiceQuery<TElement> )query;
    }

    private static DataServiceQuery<TElement> Where(
      DataServiceQuery<TElement> dataServiceQuery,
      FilterRequest request )
    {
      if( request == null )
        return dataServiceQuery;

      Debug.Assert( ( request.Filter == null ) && ( request.Expression != null ) );

      PredicateExpressionBuilder pb = new PredicateExpressionBuilder( request.Expression );
      Expression<Func<TElement, bool>> predicate = pb.ToExpression<TElement>();

      return ( DataServiceQuery<TElement> )dataServiceQuery.Where<TElement>( predicate );
    }

    private static DataServiceQuery<TElement> OrderBy(
      DataServiceQuery<TElement> dataServiceQuery,
      SortRequest request )
    {
      if( ( request == null ) || ( request.SortDescriptions.Count <= 0 ) )
        return dataServiceQuery;

      bool firstPass = true;
      IQueryable<TElement> query = dataServiceQuery;

      foreach( SortDescription sortBy in request.SortDescriptions )
      {
        // Create the key selector that will target the sorted field.
        ParameterExpression parameter = Expression.Parameter( typeof( TElement ), null );
        Expression getMember = Expression.PropertyOrField( parameter, sortBy.PropertyName );
        Type delegateType = typeof( Func<,> ).MakeGenericType( typeof( TElement ), getMember.Type );
        Expression getter = Expression.Lambda( delegateType, getMember, new ParameterExpression[] { parameter } );

        // Find out the name of the ordering method to call.
        bool ascending = ( sortBy.Direction == ListSortDirection.Ascending );
        string methodName;

        if( firstPass )
        {
          methodName = ( ascending ) ? "OrderBy" : "OrderByDescending";
        }
        else
        {
          methodName = ( ascending ) ? "ThenBy" : "ThenByDescending";
        }

        // Retrieve the method to call to sort the values.
        MethodInfo orderingMethodInfo = null;
        foreach( MethodInfo methodInfo in typeof( Queryable ).GetMethods( BindingFlags.Public | BindingFlags.Static ) )
        {
          if( ( methodInfo.Name == methodName )
            && ( methodInfo.IsGenericMethod )
            && ( methodInfo.GetGenericArguments().Length == 2 )
            && ( methodInfo.GetParameters().Length == 2 ) )
          {
            orderingMethodInfo = methodInfo.MakeGenericMethod( typeof( TElement ), getMember.Type );
            break;
          }
        }

        // Invoke the ordering method.
        query = ( IQueryable<TElement> )orderingMethodInfo.Invoke( null, new object[] { query, getter } );
        firstPass = false;
      }

      return ( DataServiceQuery<TElement> )query;
    }

    #region Private PartialDataResult Nested Type

    private class PartialDataResult
    {
      internal PartialDataResult( DataServiceQuery<TElement> dataServiceQuery )
      {
        m_dataServiceQuery = dataServiceQuery;
      }

      internal Exception Error
      {
        get
        {
          return m_error;
        }
        set
        {
          if( m_error == null )
          {
            m_error = value;
          }
        }
      }

      internal List<TElement> Data
      {
        get
        {
          return m_data;
        }
      }

      internal Nullable<long> InlineCount
      {
        get;
        set;
      }

      internal DataServiceQuery<TElement> DataServiceQuery
      {
        get
        {
          return m_dataServiceQuery;
        }
      }

      private List<TElement> m_data = new List<TElement>();
      private Exception m_error; //null
      private DataServiceQuery<TElement> m_dataServiceQuery;
    }

    #endregion
  }
}
